Przewodnik po generykach TypeScript: sk艂adnia, korzy艣ci i zaawansowane techniki obs艂ugi z艂o偶onych typ贸w danych w globalnych aplikacjach.
Generyki w TypeScript: Opanowanie z艂o偶onych typ贸w danych dla solidnych aplikacji
TypeScript, nadzbi贸r JavaScriptu, umo偶liwia programistom pisanie bardziej solidnego i 艂atwiejszego w utrzymaniu kodu dzi臋ki statycznemu typowaniu. Jedn膮 z jego najpot臋偶niejszych funkcji s膮 generyki, kt贸re pozwalaj膮 pisa膰 kod dzia艂aj膮cy z r贸偶nymi typami danych, zachowuj膮c przy tym bezpiecze艅stwo typ贸w. Ten przewodnik stanowi kompleksowe om贸wienie generyk贸w w TypeScript, koncentruj膮c si臋 na ich zastosowaniu do z艂o偶onych typ贸w danych w kontek艣cie globalnego tworzenia oprogramowania.
Czym s膮 generyki?
Generyki zapewniaj膮 spos贸b na pisanie kodu wielokrotnego u偶ytku, kt贸ry mo偶e dzia艂a膰 z r贸偶nymi typami. Zamiast pisa膰 osobne funkcje lub klasy dla ka偶dego typu, kt贸ry chcesz obs艂ugiwa膰, mo偶esz napisa膰 jedn膮 funkcj臋 lub klas臋, kt贸ra u偶ywa parametr贸w typ贸w. Te parametry typ贸w s膮 symbolami zast臋pczymi dla rzeczywistych typ贸w, kt贸re zostan膮 u偶yte podczas wywo艂ywania lub tworzenia instancji funkcji lub klasy. Jest to szczeg贸lnie przydatne w przypadku z艂o偶onych struktur danych, w kt贸rych typ danych w tych strukturach mo偶e si臋 r贸偶ni膰.
Korzy艣ci z u偶ywania generyk贸w
- Reu偶ywalno艣膰 kodu: Napisz kod raz i u偶ywaj go z r贸偶nymi typami. Redukuje to duplikacj臋 kodu i sprawia, 偶e baza kodu jest 艂atwiejsza w utrzymaniu.
- Bezpiecze艅stwo typ贸w: Generyki pozwalaj膮 kompilatorowi TypeScript egzekwowa膰 bezpiecze艅stwo typ贸w w czasie kompilacji. Pomaga to zapobiega膰 b艂臋dom wykonania zwi膮zanym z niezgodno艣ci膮 typ贸w.
- Poprawiona czytelno艣膰: Generyki sprawiaj膮, 偶e kod jest bardziej czytelny, jasno wskazuj膮c typy, z kt贸rymi maj膮 wsp贸艂pracowa膰 funkcje i klasy.
- Zwi臋kszona wydajno艣膰: W niekt贸rych przypadkach generyki mog膮 prowadzi膰 do poprawy wydajno艣ci, poniewa偶 kompilator mo偶e zoptymalizowa膰 wygenerowany kod w oparciu o u偶ywane konkretne typy.
Podstawowa sk艂adnia generyk贸w
Podstawowa sk艂adnia generyk贸w polega na u偶ywaniu nawias贸w ostrych (< >) do deklarowania parametr贸w typ贸w. Parametry te s膮 zazwyczaj nazywane T
, K
, V
itd., ale mo偶na u偶y膰 dowolnego prawid艂owego identyfikatora. Oto prosty przyk艂ad funkcji generycznej:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);
console.log(myString); // Wyj艣cie: hello
console.log(myNumber); // Wyj艣cie: 123
console.log(myBoolean); // Wyj艣cie: true
W tym przyk艂adzie <T>
deklaruje parametr typu o nazwie T
. Funkcja identity
przyjmuje argument typu T
i zwraca warto艣膰 typu T
. Wywo艂uj膮c funkcj臋, mo偶na jawnie okre艣li膰 parametr typu (np. identity<string>
) lub pozwoli膰 TypeScriptowi na jego wywnioskowanie na podstawie typu argumentu.
Praca ze z艂o偶onymi typami danych
Generyki staj膮 si臋 szczeg贸lnie cenne w przypadku z艂o偶onych typ贸w danych, takich jak tablice, obiekty i interfejsy. Przyjrzyjmy si臋 kilku typowym scenariuszom:
Tablice generyczne
Mo偶esz u偶ywa膰 generyk贸w do tworzenia funkcji lub klas, kt贸re dzia艂aj膮 z tablicami r贸偶nych typ贸w:
function arrayToString<T>(arr: T[]): string {
return arr.join(", ");
}
let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];
console.log(arrayToString(numberArray)); // Wyj艣cie: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Wyj艣cie: apple, banana, cherry
W tym przypadku funkcja arrayToString
przyjmuje tablic臋 typu T[]
i zwraca jej reprezentacj臋 w postaci ci膮gu znak贸w. Ta funkcja dzia艂a z tablicami dowolnego typu, co czyni j膮 wysoce reu偶ywaln膮.
Obiekty generyczne
Generyk贸w mo偶na r贸wnie偶 u偶ywa膰 do definiowania funkcji lub klas, kt贸re dzia艂aj膮 z obiektami o r贸偶nych kszta艂tach:
interface Person {
name: string;
age: number;
country: string; // Dodano kraj dla kontekstu globalnego
}
interface Product {
id: number;
name: string;
price: number;
currency: string; // Dodano walut臋 dla kontekstu globalnego
}
function displayInfo<T extends { name: string }>(item: T): void {
console.log(`Name: ${item.name}`);
}
let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };
displayInfo(person); // Wyj艣cie: Name: Alice
displayInfo(product); // Wyj艣cie: Name: Laptop
W tym przyk艂adzie funkcja displayInfo
przyjmuje obiekt typu T
, kt贸ry musi mie膰 w艂a艣ciwo艣膰 name
typu string. Klauzula extends { name: string }
jest ograniczeniem (constraint), kt贸re okre艣la minimalne wymagania dla parametru typu T
. Zapewnia to, 偶e funkcja mo偶e bezpiecznie uzyska膰 dost臋p do w艂a艣ciwo艣ci name
.
Zaawansowane u偶ycie generyk贸w
Generyki w TypeScript oferuj膮 bardziej zaawansowane funkcje, kt贸re pozwalaj膮 tworzy膰 jeszcze bardziej elastyczny i pot臋偶ny kod. Przyjrzyjmy si臋 niekt贸rym z tych funkcji:
Wiele parametr贸w typu
Mo偶na definiowa膰 funkcje lub klasy z wieloma parametrami typu:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
interface Name {
firstName: string;
}
interface Age {
age: number;
}
const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };
const merged = merge(person, details);
console.log(merged.firstName); // Wyj艣cie: Bob
console.log(merged.age); // Wyj艣cie: 42
Funkcja merge
przyjmuje dwa obiekty typ贸w T
i U
i zwraca nowy obiekt, kt贸ry zawiera w艂a艣ciwo艣ci obu obiekt贸w. Jest to pot臋偶ny spos贸b na 艂膮czenie danych z r贸偶nych 藕r贸de艂.
Ograniczenia generyczne
Jak pokazano wcze艣niej, ograniczenia pozwalaj膮 na zaw臋偶enie typ贸w, kt贸re mog膮 by膰 u偶ywane z generycznym parametrem typu. Zapewnia to, 偶e kod generyczny mo偶e bezpiecznie operowa膰 na okre艣lonych typach.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity([1, 2, 3]); // Wyj艣cie: 3
loggingIdentity("hello"); // Wyj艣cie: 5
// loggingIdentity(123); // B艂膮d: Argument typu 'number' nie jest przypisywalny do parametru typu 'Lengthwise'.
Funkcja loggingIdentity
przyjmuje argument typu T
, kt贸ry musi mie膰 w艂a艣ciwo艣膰 length
typu number. Zapewnia to, 偶e funkcja mo偶e bezpiecznie uzyska膰 dost臋p do w艂a艣ciwo艣ci length
.
Klasy generyczne
Generyki mo偶na r贸wnie偶 stosowa膰 w klasach:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(d => d !== item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Wyj艣cie: [ 'banana' ]
const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Wyj艣cie: [ 2 ]
Klasa DataStorage
mo偶e przechowywa膰 dane dowolnego typu T
. Pozwala to na tworzenie reu偶ywalnych, bezpiecznych typologicznie struktur danych.
Interfejsy generyczne
Interfejsy generyczne s膮 przydatne do definiowania kontrakt贸w, kt贸re mog膮 dzia艂a膰 z r贸偶nymi typami. Na przyk艂ad:
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
interface User {
id: number;
username: string;
email: string;
}
interface ErrorMessage {
code: number;
message: string;
}
function fetchUser(id: number): Result<User, ErrorMessage> {
if (id === 1) {
return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
} else {
return { success: false, error: { code: 404, message: "User not found" } };
}
}
const userResult = fetchUser(1);
if (userResult.success) {
console.log(userResult.data.username);
} else {
console.log(userResult.error.message);
}
Interfejs Result
definiuje generyczn膮 struktur臋 do reprezentowania wyniku operacji. Mo偶e zawiera膰 dane typu T
lub b艂膮d typu E
. Jest to powszechny wzorzec do obs艂ugi operacji asynchronicznych lub operacji, kt贸re mog膮 zako艅czy膰 si臋 niepowodzeniem.
Typy pomocnicze i generyki
TypeScript dostarcza kilka wbudowanych typ贸w pomocniczych, kt贸re dobrze wsp贸艂pracuj膮 z generykami. Te typy pomocnicze mog膮 pom贸c w transformacji i manipulacji typami w pot臋偶ny spos贸b.
Partial<T>
Partial<T>
sprawia, 偶e wszystkie w艂a艣ciwo艣ci typu T
staj膮 si臋 opcjonalne:
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: "Alice" }; // Poprawne
Readonly<T>
Readonly<T>
sprawia, 偶e wszystkie w艂a艣ciwo艣ci typu T
staj膮 si臋 tylko do odczytu:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // B艂膮d: Nie mo偶na przypisa膰 do 'age', poniewa偶 jest to w艂a艣ciwo艣膰 tylko do odczytu.
Pick<T, K>
Pick<T, K>
wybiera zbi贸r w艂a艣ciwo艣ci K
z typu T
:
interface Person {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };
Omit<T, K>
Omit<T, K>
usuwa zbi贸r w艂a艣ciwo艣ci K
z typu T
:
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };
Record<K, T>
Record<K, T>
tworzy typ z kluczami K
i warto艣ciami typu T
:
type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Rozszerzona lista dla kontekstu globalnego
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Rozszerzona lista dla kontekstu globalnego
type CurrencyMap = Record<CountryCodes, Currency>;
const currencyMap: CurrencyMap = {
"US": "USD",
"CA": "CAD",
"UK": "GBP",
"DE": "EUR",
"FR": "EUR",
"JP": "JPY",
"CN": "CNY",
"IN": "INR",
"BR": "BRL",
"AU": "AUD",
};
Typy mapowane
Typy mapowane pozwalaj膮 na transformacj臋 istniej膮cych typ贸w poprzez iteracj臋 po ich w艂a艣ciwo艣ciach. Jest to pot臋偶ny spos贸b na tworzenie nowych typ贸w na podstawie ju偶 istniej膮cych. Na przyk艂ad, mo偶na stworzy膰 typ, kt贸ry sprawia, 偶e wszystkie w艂a艣ciwo艣ci innego typu s膮 tylko do odczytu:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // B艂膮d: Nie mo偶na przypisa膰 do 'age', poniewa偶 jest to w艂a艣ciwo艣膰 tylko do odczytu.
W tym przyk艂adzie [K in keyof Person]
iteruje po wszystkich kluczach interfejsu Person
, a Person[K]
uzyskuje dost臋p do typu ka偶dej w艂a艣ciwo艣ci. S艂owo kluczowe readonly
sprawia, 偶e ka偶da w艂a艣ciwo艣膰 jest tylko do odczytu.
Typy warunkowe
Typy warunkowe pozwalaj膮 na definiowanie typ贸w w oparciu o warunki. Jest to pot臋偶ny spos贸b na tworzenie typ贸w, kt贸re dostosowuj膮 si臋 do r贸偶nych scenariuszy.
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string
function getValue<T>(value: T): NonNullable<T> {
if (value == null) { // Obs艂uguje zar贸wno null, jak i undefined
throw new Error("Value cannot be null or undefined");
}
return value as NonNullable<T>;
}
try {
const validValue = getValue("hello");
console.log(validValue.toUpperCase()); // Wyj艣cie: HELLO
const invalidValue = getValue(null); // To spowoduje b艂膮d
console.log(invalidValue); // Ta linia nie zostanie osi膮gni臋ta
} catch (error: any) {
console.error(error.message); // Wyj艣cie: Value cannot be null or undefined
}
W tym przyk艂adzie typ NonNullable<T>
sprawdza, czy T
jest null
lub undefined
. Je艣li tak, zwraca never
, co oznacza, 偶e typ jest niedozwolony. W przeciwnym razie zwraca T
. Pozwala to na tworzenie typ贸w, kt贸re maj膮 gwarancj臋, 偶e nie b臋d膮 nullowalne.
Dobre praktyki u偶ywania generyk贸w
Oto kilka dobrych praktyk, o kt贸rych nale偶y pami臋ta膰 podczas u偶ywania generyk贸w:
- U偶ywaj opisowych nazw parametr贸w typu: Wybieraj nazwy, kt贸re jasno wskazuj膮 przeznaczenie parametru typu.
- U偶ywaj ogranicze艅, aby zaw臋zi膰 typy, kt贸re mog膮 by膰 u偶ywane z generycznym parametrem typu: Zapewnia to, 偶e kod generyczny mo偶e bezpiecznie operowa膰 na okre艣lonych typach.
- Utrzymuj kod generyczny prosty i skoncentrowany: Unikaj nadmiernego komplikowania kodu generycznego zbyt wieloma parametrami typu lub z艂o偶onymi ograniczeniami.
- Dok艂adnie dokumentuj sw贸j kod generyczny: Wyja艣nij przeznaczenie parametr贸w typu i wszelkie u偶yte ograniczenia.
- Rozwa偶 kompromisy mi臋dzy reu偶ywalno艣ci膮 kodu a bezpiecze艅stwem typ贸w: Chocia偶 generyki mog膮 poprawi膰 reu偶ywalno艣膰 kodu, mog膮 r贸wnie偶 uczyni膰 go bardziej z艂o偶onym. Zwa偶 korzy艣ci i wady przed u偶yciem generyk贸w.
- We藕 pod uwag臋 lokalizacj臋 i globalizacj臋 (l10n i g11n): W przypadku danych, kt贸re musz膮 by膰 wy艣wietlane u偶ytkownikom w r贸偶nych regionach, upewnij si臋, 偶e Twoje generyki obs艂uguj膮 odpowiednie formatowanie i konwencje kulturowe. Na przyk艂ad formatowanie liczb i dat mo偶e znacznie r贸偶ni膰 si臋 w zale偶no艣ci od lokalizacji.
Przyk艂ady w kontek艣cie globalnym
Rozwa偶my kilka przyk艂ad贸w, jak generyki mog膮 by膰 u偶ywane w kontek艣cie globalnym:
Przeliczanie walut
interface ConversionRate {
rate: number;
fromCurrency: string;
toCurrency: string;
}
function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
return amount * rate.rate;
}
const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD is equal to ${amountInEUR} EUR`); // Wyj艣cie: 100 USD is equal to 85 EUR
Formatowanie daty
interface DateFormatOptions {
locale: string;
options: Intl.DateTimeFormatOptions;
}
function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
return date.toLocaleDateString(format.locale, format.options);
}
const currentDate = new Date();
const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };
console.log("US Date: " + formatDate(currentDate, usDateFormat));
console.log("German Date: " + formatDate(currentDate, germanDateFormat));
console.log("Japanese Date: " + formatDate(currentDate, japaneseDateFormat));
Us艂uga t艂umacze艅
interface Translation {
[key: string]: string; // Pozwala na dynamiczne klucze j臋zykowe
}
interface LanguageData<T extends Translation> {
languageCode: string;
translations: T;
}
const englishTranslations: Translation = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our website!"
};
const spanishTranslations: Translation = {
"hello": "Hola",
"goodbye": "Adi贸s",
"welcome": "隆Bienvenido a nuestro sitio web!"
};
const frenchTranslations: Translation = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre site web !"
};
const languageData: LanguageData<typeof englishTranslations>[] = [
{languageCode: "en", translations: englishTranslations },
{languageCode: "es", translations: spanishTranslations },
{languageCode: "fr", translations: frenchTranslations}
];
function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
const lang = languageData.find(lang => lang.languageCode === languageCode);
if (!lang) {
return `Translation for ${key} in ${languageCode} not found.`;
}
return lang.translations[key] || `Translation for ${key} not found.`;
}
console.log(translate("hello", "en", languageData)); // Wyj艣cie: Hello
console.log(translate("hello", "es", languageData)); // Wyj艣cie: Hola
console.log(translate("welcome", "fr", languageData)); // Wyj艣cie: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Wyj艣cie: Translation for missingKey in de not found.
Podsumowanie
Generyki w TypeScript to pot臋偶ne narz臋dzie do pisania reu偶ywalnego, bezpiecznego typologicznie kodu, kt贸ry mo偶e pracowa膰 ze z艂o偶onymi typami danych. Dzi臋ki zrozumieniu podstawowej sk艂adni, zaawansowanych funkcji i dobrych praktyk dotycz膮cych generyk贸w, mo偶na znacznie poprawi膰 jako艣膰 i 艂atwo艣膰 utrzymania aplikacji TypeScript. Podczas tworzenia aplikacji dla globalnej publiczno艣ci, generyki mog膮 pom贸c w obs艂udze r贸偶norodnych format贸w danych i konwencji kulturowych, zapewniaj膮c p艂ynne do艣wiadczenie u偶ytkownika dla wszystkich.